Skip to content

codegen: P3 B3, delete the INT32 Number consumers - #14395

Merged
proggeramlug merged 5 commits into
mainfrom
perf/p3-b3-delete-int32-consumers
Nov 10, 2026
Merged

proggeramlug merged 5 commits into
mainfrom
perf/p3-b3-delete-int32-consumers

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

P3 "canonical Numbers", DESIGN-p3 rev 3.1 §3 B3 (REVIEW-p3 R3, S3). After B1 (#14354), B2 (#14358) and B4a (#14372) nothing can produce an INT32-tagged Number, so a Number has one encoding, the plain double, for every operand: proven, unproven or ⊤. This PR deletes every test and decode of that encoding from generated code, and the helpers that emitted them.

Why it is sound to delete

Checked on main before deleting each consumer:

  • Foreign bits. The NaN-canonicalizing ingress fix (NANB, eb9ec0de82) is in main: v8_serde.rs read_double and the bun:ffi T_F64/T_F32 returns (float_value) no longer pass a payload through.
  • Producer API. JSValue::int32 is gone (B4a).
  • Every remaining writer of a 0x7FFE word (a scan of each non-test INT32_TAG |, 0x7FFE_0000_0000_0000 | in perry-runtime, perry-stdlib, perry-ffi, the ext crates and perry-abi) is one of DESIGN-p3 §1.2's internal words: overflow routes, the bound inline-args marker, the class id capture and evaluation state, the constructor identity word, the class key bits and method home word, key_attrs. The one exception is the build-time canonnum sabotage in math.rs. None is a JS value.
  • Codegen's own 0x7FFE writers stay: the async i32 control cells (read with a typed i32 load), the class first-evaluation state (read by the runtime) and the realm atom. None of them flows into a deleted consumer.

Deleted

consumer file what went
===/!== prefix, two unproven operands expr/compare.rs lower_strict_eq_inline_any the "both INT32, different bits" arm
===/!== against a Number expr/compare.rs lower_strict_eq_against_number normalize_int32_immediate (the function), the compact arm (saturating convert, round trip, payload compare)
relational against a Number expr/compare.rs lower_relational_against_number the INT32 admission and decode of the varying operand, the normalize of the proven one, the proven_expr parameter
comparison graph expr/compare_chain.rs (:149 on the design's main) the per-local INT32 test and decode; a strict-equality graph now classifies nothing
array-key normalizer expr/index_key.rs (:117-123) the boxed arm of perry_array_index_normalize_i32
typed entry, tier 2 codegen/typed_entry.rs emit_int32_box_test, emit_int32_bits_test, the int32_convert block and its phi. Tier 2 admits NaN Numbers and branches to the one fast block
typed ABI codegen/typed_abi.rs the INT32 half of emit_typed_f64_guard; emit_typed_f64_to_raw_guarded; emit_typed_i32_guard_and_raw (callers use the plain-double predicate); the tag select in emit_typed_i32_raw_assuming_guarded (one fptosi); emit_typed_arg_to_raw_after_entry_tier (now equal to emit_typed_arg_to_raw)
direct typed closure call lower_call/early_branches.rs tier 2's INT32 test, convert block and phi
scalar method inline lower_call/scalar_method.rs the decode after the Number guard
switch stmt/switch_stmt.rs emit_canonical_numeric_view and NumericView. A numeric switch compares the discriminant's own bits: every non-Number is a NaN pattern and fails each ordered fcmp
parameter entry codegen/facts_number_locals.rs emit_entry_number, function.rs, method.rs, signature_entry.rs, closure.rs the entry conversion (it was already skipped for facts-proven parameters with B1 on; now for region parameters and with B1 off too)
Number receiver kind guard lower_call/property_get/builtin_kind_guard.rs the INT32 test
check-mode witnesses facts_const_length.rs, facts_int32_locals.rs, lower_call/index_split.rs the INT32 decode; the witness is the plain exact-int32 test
switch PERRY_WP_REP_P3 codegen/facts_rep.rs canonical_numbers it "restored the tag test", which no longer exists. number_call stays under PERRY_WP_REP_P2
constants nanbox.rs INT32_MASK, INT32_TAG_TOP16_I64

Guarded arithmetic: main has no inline INT32 arm to delete. emit_js_value_is_number already rejects the whole tag band and the dynamic arm is a runtime call. Only its comment changed.

Left, with the reason

  • Calls into the runtime's own guards: js_typed_f64_arg_guard / js_typed_f64_arg_to_raw (the Map/Set number-key paths in math_simple.rs, bigint_set.rs, logical_collections.rs, method_override.rs) and js_typed_i32_arg_guard / _to_raw (scalar_method.rs). Their INT32 arm is inside the runtime functions, which is B4b. Once B4b lands, the to_raw call is an identity and goes with it.
  • INT32_TAG / INT32_TAG_I64: kept for the internal words. The rename is B4b.
  • Comments that list INT32 as one member of the tag band next to a band test (index.rs, index_set_*, member_update.rs, …): the tests are band tests, not tests of the tag.

No switch

A pure deletion has nothing to switch. Every comparison below is the branch against the main build at the merge base.

Controls

tests/parity/language/number/test_p3_b3_top_operands.ts drives each deleted path with unproven operands (values from JSON.parse and a mixed array: 0, ±1, 7, 2.5, int32 min and max, 2^32, 1e300, -0, NaN, ±Infinity, numeric and non-numeric strings, null, undefined, booleans, a BigInt, an object with valueOf, an array, a Symbol):

  • ===/!== against int literals, a fraction and a Number local, both orders; relationals against a Number; every kind against every kind for === and <;
  • a comparison graph; a dense numeric switch and a mixed-literal tower;
  • an unproven array key, read and store;
  • typed Number and int32 entries of functions, methods and closures, called through any;
  • a method on a receiver that may be a Number; arithmetic over unproven leaves.

The negative controls are the non-Numbers: "7" === 7 is false, switch ("7") takes the string case, null >= 0 is true, undefined < 1 false, a BigInt and a Symbol take their own arms. Output equals node in normal mode, check mode, with moving-GC loop polls, with PERRY_FACTS_CANONICAL_NUMBERS=0, and on main.

Results (qb9 x86-64, instructions:u; branch a0923eb11c vs main 5de5bb7544, its merge base)

workload branch (M) main (M) change
search 250000 34 750.8 35 630.4 −2.47%
perft 4 818.8 844.6 −3.05%
evaluate 100000 3 166.4 3 188.8 −0.70%
legal 10000 1 179.1 1 204.4 −2.10%

Two runs per arm, identical to 0.1 M; checksums equal. Chess IR: 125 INT32 identity tests on main, 0 on the branch; 707 fewer IR lines (and −146, icmp −131, select −101, sitofp −88, or −58).

gate result
hello / hello2 byte-identical main / branch (md5 dc07ba3f94af, 9bb2e64cc5e9); .text 1238 and 5 907 390; callgrind startup Ir 127 145 and 623 941, the same in all 3 runs of both arms
fixtures vs node v26.12.1 the B3 control and the B1, B2, B4a controls: MATCH in all five arms (branch, check mode, loop polls, PERRY_FACTS_CANONICAL_NUMBERS=0, main)
vf corpus, PERRY_VERIFY_FACTS=1 n=1538, json=0, abort=0, rc86=0, crc!=0=2 (the known pair: test_gap_s8b_computed_fields, test_gap_10554_fn_identity_own_module)
S0 67 executable controls pass
chess in check mode perft, legal, search and evaluate rc 0, no misses
parity corpus IR census, branch vs main n=2545: IR differs in 1478, compile rc in 0, output in 10, none real (below)
lost sites, tssrc lost 0, gained 0; proven 24 552 = 24 552
suites (release) perry-wpfacts 2196 passed, perry-codegen lib 2838, perry-hir 881, perry-runtime 6641; 0 failed
-D warnings perry-hir, perry-wpfacts, perry-codegen, perry: 0 errors
scripts/check_file_size.sh passes; two allowlist counts re-recorded (codegen/mod.rs 3588 → 3586, tests/native_proof_regressions.rs 15680 → 15678)

The 10 output diffs: 5 programs fail to compile in both arms (crc 1/1: test_gap_s8b_computed_fields, test_gap_10554_fn_identity_own_module, test_gap_cjs_factory_slot, test_gap_11275_reexport_value_kinds, test_issue_5431_dollar_named_export_call), so there is no output to compare. 4 print time or random values (test_issue63_arr, test_date, test_math, test_issue58_object_string). test_gap_this_param_receiver_routes differs only in the PID of a deprecation warning line.

Size (every executable section; branch vs main)

program branch main change
chess 7 609 435 7 616 319 −6 884
three.js skin 25 309 567 25 378 815 −69 248 (−0.27%)
tssrc 63 960 837 64 354 367 −393 530 (−0.61%)
Supabase CLI 217 695 117 219 394 141 −1 699 024 (−0.77%)

All four shrink. In skin, .text grows by 11 456 B while perry_js_text shrinks by 80 704 B; the per-symbol cause of the .text part was not investigated.

The canonnum sabotage

scripts/p3-canonnum-runtime.sh built the runtime whose js_math_round returns an INT32 word (archive mtime check passed). The S0 false arm canonical-number-negative reports a false proof on the branch, and the clean arm passes. But the S0 script's assertion fails (rc 1) on both the branch and main 5de5bb7544: the miss is now reported first by the array-representation witness (predicate "array rep", from #14390), where the script expects "canonical number". The forged word is caught; the script's expectation is stale on main and is not changed here.

Tests changed

  • expr/compare_chain_tests.rs, compare_tests.rs, hit_path_tests.rs, codegen/typed_abi/tests.rs, facts_number_locals_tests.rs, stmt/byte_result_tests.rs: they asserted the INT32 test or decode; they now assert its absence (and the one plain-number classification where there is one).
  • tests/native_proof_regressions.rs: unboxes_number_arg (INT32 test plus sitofp) is replaced by tests_int32_tag, asserted absent. This file has 23 failing tests on main on macOS; the same 23 fail on the branch.
  • macOS: perry-codegen lib has 15 failing tests on main; the same 15 fail on the branch. On qb9 the lib suite is 2838/0.

Not re-measured

The final head also merges main 8e1c285569 (PR-C2 and three wpfacts commits; a clean merge, cargo check -p perry-codegen --all-targets clean on macOS). The gates above ran on a0923eb11c, before that merge.

Ralph Küpper added 5 commits November 10, 2026 01:03
Since P3 B4a nothing can produce an INT32-tagged Number, so a Number has one
encoding for every operand, proven or not. Compiled code no longer tests for
the 0x7FFE word or decodes it:

- compare: the both-INT32 arm of the === prefix, normalize_int32_immediate,
  the compact arm of strict equality against a Number, the INT32 arm of the
  relational against a Number
- compare_chain: the per-local decode of a comparison graph
- index_key: the boxed arm of perry_array_index_normalize_i32
- typed_abi / typed_entry: emit_int32_box_test, emit_int32_bits_test, the
  tier-2 convert block, emit_typed_f64_to_raw_guarded,
  emit_typed_i32_guard_and_raw, emit_typed_arg_to_raw_after_entry_tier
- the direct typed closure call's tier 2 (early_branches)
- switch: emit_canonical_numeric_view and NumericView
- parameter entry: emit_entry_number, the closure region conversion
- the Number receiver kind guard, the const-length witness decode
- PERRY_WP_REP_P3 (facts_rep::canonical_numbers): it restored a tag test
  that no longer exists
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@proggeramlug
proggeramlug merged commit 7cd5850 into main Nov 10, 2026
92 of 102 checks passed
@proggeramlug
proggeramlug deleted the perf/p3-b3-delete-int32-consumers branch November 10, 2026 03:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant